這份筆記從 Abby 手寫的大綱整理而成,並補上三件原稿缺漏或寫錯的地方:
a. 原始型別漏掉 BigInt,實際上是 7 種而不是 6 種
b. React 星星評分的三種寫法裡,寫法 1 與寫法 3 都是壞的,本文附上實跑證據與修正
c. WeakMap 範例裡變數名打錯(vm宣告卻用wm呼叫),實際會噴ReferenceError而不是TypeError
📖 MDN|JavaScript data types and data structures | Glossary: Primitive | typeof 運算子
JavaScript 的值
│
├── 原始型別 Primitive ── 7 種 ── 不可變 immutable ── 值本身直接表示
│ │
│ ├── String "abc"
│ ├── Number 7 IEEE 754 雙精度浮點數
│ ├── BigInt 7n ES2020 新增
│ ├── Boolean true
│ ├── Symbol Symbol() 永遠唯一
│ ├── undefined 引擎說「這裡沒東西」
│ └── null 開發者說「我刻意清空」
│ ⚠️ typeof 卻回傳 "object"(1995 年的 bug)
│
└── 物件 Object ── 1 種 ── 可變 mutable ── 變數存的是參考 reference
│
├── Object 物件字面量 { }
├── Array 陣列 [ ] ⚠️ typeof 也是 "object"
├── Function 函式 ⚠️ typeof 特別回傳 "function"
├── Map / Set 鍵值集合
├── WeakMap / WeakSet 弱引用集合
└── Date / RegExp / Promise / Error ...
一句話定義
typeof 的實測結果(Node v22.22.2):
["str", 1, true, Symbol(), null, undefined, 10n, {}, [], function(){}, new Map()]
.map(v => typeof v)
// string | number | boolean | symbol | object | undefined | bigint | object | object | function | object
原始的 typeof 對照表截圖:

⚠️ 兩個著名的坑
typeof null === "object" — 這是 1995 年 JS 第一版留下的 bug,值的低三位標籤是 000 代表物件,而 null 的機器表示剛好是全 0,所以被誤判。ECMA 已經明講不會修,因為修了會炸掉整個網路。判斷 null 要用 value === null。typeof function(){} === "function" — 函式其實是物件的子型別,但因為它有 [[Call]] 內部方法,typeof 特別給它一個名字。📖 MDN|String | String.prototype.substring() | String.prototype.slice() | String.prototype.concat() | 樣板字面值 Template literals
const s = "JavaScript";
s.substring(0, 4); // "Java" 取子字串(含頭不含尾)
s.slice(-6); // "Script" slice 接受負數索引,substring 不接受
"con".concat(78); // "con78" 把多個值接成一個新字串
substring(start, end):取區間,不接受負數,負數會被當成 0。slice(start, end):功能類似但接受負數,-1 代表倒數第一個。實務上 slice 用得比 substring 多。concat():回傳新字串,原字串不動(因為原始型別不可變)。實務上大家幾乎都用 + 或樣板字串 `${a}${b}`,concat 只在需要串很多段時偶爾出現。自動裝箱(autoboxing):"abc".length 之所以能動,是因為引擎在讀屬性的瞬間,臨時包一個 String 包裝物件出來,取完就丟。所以下面這行不會報錯但也沒用:
const s = "abc";
s.foo = 1; // 非嚴格模式下靜靜失敗
console.log(s.foo); // undefined(那個臨時包裝物件已經被丟掉了)
關聯:這件事在
iThome鐵人賽-2026/文章-從一個SyntaxError讀懂物件字面量與自動裝箱.md有更完整的推導,兩篇講的是同一個機制的不同切面 — 這裡講「為什麼原始型別有方法可以呼叫」,那裡講「為什麼包裝物件的生命週期短到你抓不住它」。
📖 MDN|Number | Number.MAX_SAFE_INTEGER | NaN | Number.isNaN() | Infinity
JavaScript 的 Number 只有一種:IEEE 754 雙精度浮點數(64 bit)。沒有 int 和 float 之分。
三個特殊值:
+Infinity:1 / 0、Number.MAX_VALUE * 2
-Infinity:-1 / 0
NaN(Not a Number):0 / 0、"con" - 78、parseInt("abc")
console.log("con" - 78); // NaN 減法會強制轉數字,轉不動就 NaN
console.log("78" - 8); // 70 這個轉得動減法會這樣但加法不會這樣
//+法(字串連結符號)就只有字串的合併
console.log("78"+"78") // 7878
console.log(NaN === NaN); // false 唯一一個不等於自己的值
console.log(Number.isNaN(NaN)); // true 正確的檢查方式
console.log(isNaN("abc")); // true ⚠️ 舊版全域 isNaN 會先做型別轉換,容易誤判
console.log(0.1 + 0.2); // 0.30000000000000004 浮點數精度問題
檢查 NaN 的三種寫法
Number.isNaN(x) — ES6 之後的正解,不做型別轉換x !== x — 利用「NaN 不等於自己」的特性,最原始但最可靠isNaN(x) — ⚠️ 全域版本會先 Number(x),所以 isNaN("abc") 也是 true,通常不是你要的📖 MDN|BigInt | BigInt 字面值語法 | Number.MAX_SAFE_INTEGER | Number.isSafeInteger() | TypeError: can't convert BigInt to number
BigInt 是 ES2020 加進來的第 7 種原始型別,語法是數字結尾加一個 n:
const big = 9007199254740993n;
typeof big; // "bigint"
big + 1n; // 9007199254740994n
BigInt(42); // 42n ← 也可以用函式轉換
BigInt("42"); // 42n
// new BigInt(42); ❌ TypeError: BigInt is not a constructor
Number 是 IEEE 754 雙精度浮點數,尾數(mantissa)只有 52 bit,所以能精確表示的整數上限是 Number.MAX_SAFE_INTEGER = 9007199254740991(2^53 − 1,約 9 千兆)。超過就開始靜靜掉精度:
console.log(9007199254740993); // 9007199254740992 ← 被改掉了
console.log(9007199254740992 === 9007199254740993); // true ← 兩個不同的數字變成同一個
console.log(Number.isSafeInteger(9007199254740993)); // false
console.log(9007199254740993n); // 9007199254740993n ← BigInt 才對
⚠️ 注意這裡完全不會報錯,這才是可怕的地方。
int64、Java 的 Long、PostgreSQL / MySQL 的 bigint,只要你需要對它加減乘除而不只是傳遞Number 完全裝不下process.hrtime.bigint() 直接回傳 BigIntDate.now() ≈ 1.788 × 10^12,離上限還有兩萬倍的空間5n / 2n 是 2n 不是 2.5
1n + 1 // ❌ TypeError: Cannot mix BigInt and other types
Math.max(1n, 2n) // ❌ TypeError: Cannot convert a BigInt value to a number
JSON.stringify({ v: 1n }) // ❌ TypeError: Do not know how to serialize a BigInt
5n / 2n // 2n ← 直接截斷,沒有小數
1n == 1 // true ← 寬鬆相等會轉換
1n === 1 // false ← 嚴格相等看型別,不相等
| 代價 | 說明 |
|---|---|
| 不能和 Number 混用 | 每一次運算都要顯式 BigInt() 或 Number() 轉換,程式碼變醜 |
整套 Math 都拒收 |
Math.max、Math.round、Math.sqrt、Math.abs 全部不能用 |
JSON.stringify 直接丟錯 |
要自己寫 replacer 把它轉成字串 |
| 沒有小數 | 除法一律截斷 |
| 比較慢 | 任意精度運算不是 CPU 原生指令,是軟體模擬的 |
=== 跨型別必為 false |
1n === 1 是 false,容易在條件判斷踩到 |
這是前後端分離專案幾乎一定會遇到一次的 bug:
const raw = '{"id": 1234567890123456789, "name": "abby"}';
JSON.parse(raw).id
// 1234567890123456800 ← 最後三位被改掉,而且完全不報錯
症狀:前端拿這個 ID 去查詢,後端回「查無此資料」。你把 ID 印出來看,跟資料庫裡的長得幾乎一模一樣,只有最後幾位不同 —— 這種 bug 極難抓,因為你的眼睛會自動忽略那幾位數字。
為什麼 BigInt 救不了你:JSON.parse 在你拿到值之前就已經把它轉成 Number 破壞掉了。等你想用 BigInt 包,資訊已經沒了。
正解是請後端把 int64 序列化成字串:
JSON.parse('{"id":"1234567890123456789"}').id // "1234567890123456789" ✅ 完整
前端就把它當字串處理。這也是為什麼很多 API 設計規範會明訂「ID 一律用 string 傳輸」。
如果你真的沒辦法改後端,退路是用支援 BigInt 的 JSON 解析器(例如 json-bigint),或用正規表示式在 JSON.parse 之前先把長數字包成字串 —— 但兩者都是繞路,能改後端就改後端。
需要處理的整數是不是超過 9007199254740991(約 9 千兆)?
├─ 否 → 用 Number,不要想太多
└─ 是 → 你需要對它「做算術」還是只是「傳遞它」?
├─ 只是傳遞(ID、序號) → 用 string,請後端配合
└─ 需要算術(位移、加總、模運算)→ 用 BigInt,並接受上面那六個代價
對應題目:LeetCode 2666. Allow One Function Call 雖然不直接考 BigInt,但 JavaScript 30 Days 系列裡多題的邊界測資會踩到
Number的精度上限,寫的時候留意一下就會有感。
📖 MDN|Boolean | Glossary: Truthy | Glossary: Falsy | 空值合併運算子 ?? | 邏輯或 ||
falsy 只有 8 個,其餘全部 truthy:
false
0
-0
0n(BigInt 的零)""(空字串)null
undefined
NaN
if ([]) console.log("空陣列是 truthy"); // ✅ 會印出來
if ({}) console.log("空物件是 truthy"); // ✅ 會印出來
if ("0") console.log("字串零是 truthy"); // ✅ 會印出來
⚠️ 實務最常踩的坑:用 || 給預設值時,0 和 "" 會被誤判成「沒填」。
// ❌ 使用者真的填 0 的時候會被吃掉
const count = props.count || 10;
// ✅ 空值合併運算子 ?? 只有 null / undefined 才走右邊
const count = props.count ?? 10;
對應題目:LeetCode 2705. Compact Object — 就是在考「遞迴移除物件裡所有 falsy 值」,寫完會對 falsy 清單非常有感。
== 是用 truthy / falsy 判斷的嗎?不是📖 MDN|等於 == | 嚴格等於 === | Equality comparisons and sameness(四種相等演算法比較) | Object.is() | Type coercion 型別轉換
📖 規範|ECMA-262: IsLooselyEqual (==) | IsStrictlyEqual (===)
== 確實會做型別轉換(coercion),但它轉的方向是「數字/原始值」,不是「布林」。 它從頭到尾沒有呼叫 Boolean(),所以跟 truthy / falsy 是兩套完全獨立的規則。
如果 == 真的是用 truthy / falsy 判斷,下面這兩組不可能同時成立:
Boolean([]); // true ← 空陣列是 truthy
[] == false; // true ← 卻等於 false ?!
Boolean(null); // false ← null 是 falsy
null == false; // false ← 卻不等於 false ?!
undefined == false; // false ← 同樣不等於 false
[] 是 truthy,但 [] == false 是 true
null 是 falsy,但 null == false 是 false
兩組結論完全相反,所以 == 一定不是走 truthy / falsy。
== 到底怎麼判斷:IsLooselyEqual 演算法規範裡的步驟(ECMA-262 §7.2.15),照順序往下比:
=== 的規則比,不做任何轉換null、一邊 undefined → 回傳 truenull 與 undefined 只跟彼此相等,跟 0、""、false 全都不相等)false → 0,true → 1),然後回到第一步重新比一次
Symbol.toPrimitive → valueOf → toString),然後回到第一步false
關鍵在第 d 步:布林值是被轉成數字,而不是把另一邊轉成布林。這就是所有誤會的根源。
[] == false 拆開看[] == false
→ 第 d 步:false 是 Boolean,轉成 Number 0
[] == 0
→ 第 e 步:[] 是 Object,做 ToPrimitive
[].valueOf() 回傳 [] 本身,不是原始值 → 繼續
[].toString() 回傳 "" → 拿到原始值
"" == 0
→ 第 c 步:String 轉 Number,Number("") 是 0
0 == 0
→ 第 a 步:型別相同,相等
true
⚠️ 全程沒有任何一步呼叫 Boolean()。[] 是不是 truthy 從來沒有被問過。
null == false 為什麼是 falsenull == false
→ 第 b 步:一邊是 null,但另一邊是 false 不是 undefined → 不適用
→ 第 c 步:null 不是 Number 也不是 String → 不適用
→ 第 d 步:false 轉成 0
null == 0
→ null 只跟 undefined 相等,跟 0 不相等
→ 第 g 步:回傳 false
false
null 在 == 的世界裡是個獨行俠:它只跟 undefined 相等,跟任何其他值都不相等 —— 就算那個值也是 falsy。
== false 是兩回事| 值 | Boolean(v)(truthy 判斷) |
v == false(loose equality) |
一致嗎 |
|---|---|---|---|
false |
false |
true |
✅ |
0 |
false |
true |
✅ |
-0 |
false |
true |
✅ |
0n |
false |
true |
✅ |
"" |
false |
true |
✅ |
null |
false |
false |
❌ 不一致 |
undefined |
false |
false |
❌ 不一致 |
NaN |
false |
false |
❌ 不一致 |
[] |
true |
true |
❌ 不一致 |
"0" |
true |
true |
❌ 不一致 |
" "(空白字元) |
true |
true |
❌ 不一致 |
[0] |
true |
true |
❌ 不一致 |
{} |
true |
false |
✅ |
"false" |
true |
false |
✅ |
15 個值裡有 7 個不一致,比例接近一半 —— 這就是為什麼不能把兩者混為一談。
"1" == true // true "1" → 1,true → 1
"abc" == true // false true → 1,"abc" → NaN,NaN 不等於任何東西
2 == true // false true → 1,2 !== 1(⚠️ 不是「2 是 truthy 所以等於 true」)
"\n" == 0 // true Number("\n") 是 0,因為前後空白會被去掉
[0] == false // true [0] → "0" → 0
[1] == true // true [1] → "1" → 1
null == 0 // false null 只跟 undefined 相等
NaN == NaN // false NaN 不等於自己
[] == ![] // true ![] 是 false(因為 [] 是 truthy),然後 [] == false 走上面那條路
⚠️ 特別看 2 == true 是 false 這一題:2 明明是 truthy,true 也是 true,如果按照 truthy 判斷應該相等 —— 但實際上是 false,因為 true 被轉成 1,而 2 !== 1。
if (x) 呢?那個才是 truthy / falsy這是最需要分清楚的一組:
if (x) { } // ← 這裡才是 ToBoolean,走 truthy / falsy 規則
x == false // ← 這裡是 IsLooselyEqual,走轉數字的規則
x === false // ← 這裡完全不轉換,型別不同就直接 false
const arr = [];
if (arr) console.log("進得去"); // ✅ 會印出來([] 是 truthy)
console.log(arr == false); // true(但它又「等於」false)
同一個 [],在 if 裡是真的,在 == 裡等於假的 —— 兩套規則各走各的。
===,除非你有明確理由== 的場合是 x == null —— 它等價於 x === null || x === undefined,是檢查「空值」最短的寫法。ESLint 的 eqeqeq 規則預設也有 "smart" 選項專門放行這一種if (x) 或 x ?? 預設值,不要用 x == false 這種寫法x != null 是很常見的 guard,例如 if (data != null) { ... } 一次擋掉 null 與 undefined// ✅ 這個 == 是可以接受的
if (value == null) return "沒有資料"; // 同時擋掉 null 與 undefined
// ❌ 這種就別寫了
if (count == false) { } // 意圖不明,而且 null 會漏掉
// ✅ 改成
if (!count) { } // 明確在問 truthy / falsy
if (count === 0) { } // 明確在問「是不是 0」
| 演算法 | 寫法 | 做型別轉換 | NaN 等於 NaN |
+0 等於 -0 |
|---|---|---|---|---|
| Loose equality | == |
✅ 轉數字/原始值 | ❌ | ✅ |
| Strict equality | === |
❌ | ❌ | ✅ |
| SameValueZero | [].includes()、Map / Set 的 key 比對 |
❌ | ✅ | ✅ |
| SameValue | Object.is() |
❌ | ✅ | ❌ |
[NaN].includes(NaN); // true SameValueZero 認得 NaN
[NaN].indexOf(NaN); // -1 indexOf 用的是 ===,認不得
Object.is(NaN, NaN); // true
Object.is(+0, -0); // false 只有它區分正負零
new Set([NaN, NaN]).size; // 1 SameValueZero,所以只留一個
⚠️ Object.is 就是 React 判斷 state 有沒有變的那個函式,所以理解這張表對你寫 React 有直接幫助。
對應題目:LeetCode 2704. To Be Or Not To Be — 實作
toBe(用===)與notToBe,很短但會逼你把「相等」想清楚。
實跑驗證的完整程式碼在 demo-01-primitives-typeof.js 的最後一段。
📖 MDN|Symbol | Symbol.for()(全域註冊表) | Well-known symbols 知名符號 | Symbol.iterator | Symbol.species
Symbol 的核心是唯一性:Symbol("id") !== Symbol("id"),括號裡的字串只是給人看的描述(description),不參與比對。
📖 MDN|Map | Map.prototype.set() | Map.prototype.forEach() | Map.prototype.keys()
const symKey1 = Symbol("id");
const symKey2 = Symbol("id"); // 描述一樣,但是完全不同的兩個 Symbol
const myMap = new Map();
myMap.set(symKey1, "User_001");
myMap.set(symKey2, "User_002");
console.log(myMap.get(symKey1)); // "User_001"
console.log(myMap.get(symKey2)); // "User_002"
console.log(myMap.size); // 2 — 證明兩個 key 沒有互相覆蓋
console.log(myMap); // Map(2) {Symbol(id) => 'User_001', Symbol(id) => 'User_002'}
Map 的 key 可以是任何值(物件、Symbol、NaN 都行),這是它和普通物件最大的差異 — 普通物件的 key 只能是 string 或 Symbol,其他型別會被 String() 轉掉。
myMap.keys() 和 myMap.forEach() 都能正常迭代到 Symbol key,實測:
const s = Symbol("id");
const m = new Map([[s, "v"]]);
m.forEach((v, k) => console.log(String(k), v)); // Symbol(id) v
📖 MDN|Object.getOwnPropertySymbols() | Reflect.ownKeys() | Object.keys() | 列舉性與所有權 Enumerability and ownership
Symbol 屬性是隱藏的,一般的列舉手段全部看不到它:
const s = Symbol("id");
const o = { [s]: 1, name: "abby" };
Object.keys(o); // [ 'name' ] ❌ 看不到
JSON.stringify(o); // {"name":"abby"} ❌ 看不到
for (const k in o) {} // ❌ 看不到
Object.getOwnPropertySymbols(o); // [ Symbol(id) ] ✅ 只拿 Symbol
Reflect.ownKeys(o); // [ 'name', Symbol(id) ] ✅ 全部一起拿
為什麼要設計成隱藏:讓函式庫可以在使用者的物件上掛「內部用」的欄位,而不會污染使用者的 Object.keys() 或 JSON.stringify() 結果。這也是 Symbol.iterator、Symbol.species 這類「知名 Symbol(well-known symbols)」的用法。
關聯:
iThome鐵人賽-2026/Map-Symbol-species-規範裡沒人用的屬性.md講的就是 well-known symbol 的其中一個 — 那篇是「知名 Symbol 怎麼被規範呼叫」,這裡是「Symbol 這個型別本身的性質」,先讀這裡再讀那裡比較順。
📖 MDN|null(注意:它是關鍵字所以歸在 Operators 底下)| undefined(它是全域屬性所以歸在 Global_Objects 底下)| 可選鏈 ?. | setTimeout() | clearTimeout()
null |
undefined |
|
|---|---|---|
| 語意 | 我刻意把它清空 | 引擎說「這裡沒東西」 |
| 誰放的 | 開發者手動指定 | 引擎預設(未賦值變數、沒有 return、找不到的屬性、少傳的參數) |
typeof |
"object"(歷史 bug) |
"undefined" |
JSON.stringify |
保留成 null |
整個鍵會消失 |
== 互相比較 |
null == undefined 是 true |
同左 |
=== 互相比較 |
null === undefined 是 false |
同左 |
null 最典型的用途是「任何時刻最多只有一個有意義的值,而且不需要保留歷史」:
function debounce(fn, delay) {
let timer = null; // 一開始沒有排程中的任務
return function (...args) {
if (timer !== null) {
clearTimeout(timer); // 使用者又打字了,把上一個排程取消
}
timer = setTimeout(() => {
fn.apply(this, args);
timer = null; // 執行完把狀態清回「沒有排程」
}, delay);
};
}
const search = debounce((keyword) => console.log("查詢", keyword), 300);
input.addEventListener("input", (e) => search(e.target.value));
這段程式的意思是:使用者停止打字滿 300 毫秒之後,才真的送出一次查詢。搜尋框、表單即時驗證是最常見的場景。
timer 之所以用 null 而不是 undefined,是因為 null 傳達的是「我知道這裡該有東西,我現在主動把它設成空」,undefined 傳達的是「我沒管過這裡」。
關聯:
iThome鐵人賽-2026/文章-閉包活多久-從debounce的timer看Environment-Record存活時間.md用的就是同一段 debounce,那篇問的是「這個timer變數活到什麼時候才被回收」,剛好接上下一節。
對應題目:LeetCode 2627. Debounce — 直接就是實作這個函式。
📖 MDN|Closures 閉包 | Memory management 記憶體管理與 GC | 作用域 Scope
📖 規範|ECMA-262: Environment Records | Function Environment Records
Abby 原稿的說法:
基礎型別用 let / var / const 會在記憶體裡劃出一個專屬區域,通常在 stack;但如果是閉包中內層函式引用的外層變數,該值會被提升到 heap,直到閉包被垃圾回收為止。這是透過 V8 在編譯階段建立
[[Environment]]完成的。
這個心智模型方向正確,但有兩處需要修正:
a. 「原始型別一律放 stack」是教學用的簡化模型,不是 V8 的實作。V8 裡:
b. 「值被提升到 heap」的說法不精準。實際上是 V8 在解析階段(parse,不是編譯階段)就分析出「哪些變數被內層函式引用」,這些變數從一開始就被配置在 heap 上的 Context 物件裡(叫 context allocation);沒被引用的變數才留在 stack frame。所以不是先在 stack、之後搬家,而是一開始就決定放哪。
正確的機制描述
【建立時發生什麼】
函式被建立
│
▼
函式物件取得內部欄位 [[Environment]] ← 掛在「函式物件」上,不是掛在變數上
│
▼
它指向「建立當下」所在的 Environment Record(環境紀錄)
│ ← ES5 時代叫 Scope 物件,ES6 之後改用這個名字
▼
Environment Record 內有 [[OuterEnv]] 指向外層
│
▼
一層一層串成「單向鏈結串列 singly linked list」= Scope Chain 作用域鏈
│
▼
只要還有函式持有引用,整條鏈就不會被 GC 回收 ← 這就是閉包造成記憶體洩漏的原理
【查一個變數時怎麼走】
[內層函式]
│ [[Environment]]
▼
[Env Record:debounce 的內部] ──── timer 住這裡
│ [[OuterEnv]]
▼
[Env Record:模組 / 外層函式] ──── fn、delay 住這裡
│ [[OuterEnv]]
▼
[Global Environment Record] ──── console、setTimeout 住這裡
│ [[OuterEnv]]
▼
null ──── 到這裡還沒找到就 ReferenceError
沿著鏈往外走,找到就停 ── 所以巢狀越深,查外層變數的路徑越長
[[Environment]] 是函式物件上的內部欄位(internal slot),不是變數上的[[OuterEnv]] 串成單向鏈結串列(singly linked list),這就是作用域鏈(scope chain)addEventListener 或 setTimeout 持有),整條鏈就不會被回收 — 這就是閉包造成記憶體洩漏的原理📖 MDN|Object | Working with objects 物件操作指南 | Object.freeze() | Object.seal()
const o = { name: "Christine", age: 20 };
o.name = "Abby"; // 可變(mutable)
const o2 = o;
o2.name = "Look";
console.log(o.name); // "Look" — o 和 o2 指向同一個本體
📖 MDN|Array | Array() 建構函式 | Array.isArray() | Array.from() | Array.prototype.fill() | Indexed collections(含 sparse arrays)
MDN 的建議原文:
Use the array constructor only when you need to create an empty array of a specific length.
(只有在需要建立指定長度的空陣列時,才考慮使用new Array(n))
new Array() 的參數陷阱傳入單一數字跟傳入其他任何東西,行為完全不同:
// 傳一個數字 → 建立指定長度的稀疏陣列(sparse array)
console.log(new Array(3)); // [ <3 empty items> ]
console.log(new Array(3).length); // 3
console.log(new Array(3)[0]); // undefined
console.log(0 in new Array(3)); // false ← 關鍵:索引 0 根本不存在
// 傳一個非數字 → 建立長度 1、內含該元素的陣列
console.log(new Array("3")); // [ '3' ] length 1
console.log(new Array(true)); // [ true ] length 1
console.log(new Array({ name: "Christine" })); // [ { name: 'Christine' } ]
// 傳兩個以上 → 就是普通的元素列表
console.log(new Array(1, 2, 3)); // [ 1, 2, 3 ]
⚠️ 這個「單一數字特例」只適用於 Array 建構函式,方括號字面量 [3] 永遠是「長度 1、裡面裝著 3」。
length 大於實際存在的索引數量,也就是裡面有至少一個空位(empty slot / hole)
const sparse = new Array(2); // [ <2 empty items> ] ← 稀疏
const dense = [undefined, undefined]; // [ undefined, undefined ] ← 密集
console.log(0 in sparse); // false 索引不存在
console.log(0 in dense); // true 索引存在,只是值是 undefined
核心關鍵:重點在「有沒有空位」,而不是「是不是全部都是空的」。 只要有一個洞就是稀疏陣列。
為什麼要在意:因為大部分陣列方法會跳過空位:
let n = 0;
new Array(3).map(() => { n++; });
console.log(n); // 0 ← callback 一次都沒被呼叫
會跳過空位的:map forEach filter reduce some every
不會跳過的:join(當成空字串)、fill、find / findIndex(當成 undefined)、for...of(當成 undefined)、展開運算子 ...(當成 undefined)
// ❌ 原稿寫法 1:箭頭函式用了大括號卻沒 return,map 出來全是 undefined
{new Array(5).fill(0).map((_, index)=>{
<Star key={index} />
})}
// ✅ 修正:改用小括號(隱式回傳)或補上 return
{new Array(5).fill(0).map((_, index) => (
<Star key={index} />
))}
// ✅ 寫法 2:Array.from — 最語意清楚,而且可以直接傳 mapFn 省掉一次 .map
{Array.from({ length: 5 }, (_, index) => (
<Star key={index} />
))}
// ❌ 原稿寫法 3:括號位置錯了,map 在展開之前就對稀疏陣列跑,全部被跳過
{[...Array(5).map((_, index) => <Star key={index} />)]}
// ✅ 修正:先展開讓陣列變密集,再 map
{[...Array(5)].map((_, index) => (
<Star key={index} />
))}
實跑驗證(Node v22.22.2):
[...Array(5).map((_,i)=>i)] // [null,null,null,null,null] ← JSON 化後的空位,等同全是 undefined ❌
[...Array(5)].map((_,i)=>i) // [0,1,2,3,4] ✅
new Array(5).fill(0).map((_,i)=>i) // [0,1,2,3,4] ✅(fill 把洞填實了)
Array.from({length:5},(_,i)=>i) // [0,1,2,3,4] ✅
三種寫法差別的一句話總結:
fill() — 把稀疏變密集,然後正常 mapArray.from({length:n}, fn) — 從一個「類陣列物件」建立,過程中根本沒有洞
[...Array(n)] — 展開運算子會把洞讀成 undefined,等於一次填實typeof new Array(...) 是 "object"const arr = new Array({ name: "Christine" });
console.log(typeof arr); // "object" ← 不管裡面裝什麼都一樣
console.log(Array.isArray(arr)); // true ✅ ECMA-262 提供的正解
console.log(arr instanceof Array); // true ⚠️ 跨 iframe / 跨 realm 會失效
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
因為陣列本質上就是一種特殊物件(exotic object):索引其實是字串 key,只是多了一個會自動同步的 length。typeof 沒辦法區分它和普通物件,所以規範另外提供 Array.isArray()。
instanceof 的問題在於它比對的是 prototype 鏈,而每個 iframe / worker 有自己的 Array 建構函式,所以從別的 realm 傳過來的陣列會被判成 false。Array.isArray() 檢查的是內部標記,不受影響 — 這是它存在的唯一理由。
React 要求的是「在陣列或迭代渲染時的 JSX 元素」必須有 key。單獨渲染一個元件(<MyComponent />)不需要 key;只有用 .map() 或迴圈產生一組元件時,每一項才必須有唯一的 key。
key 的作用是讓 React 的 diff 演算法在重新渲染時能認出「這一項還是原來那一項」,避免整段拆掉重建(以及連帶把 input 的輸入狀態弄丟)。
📖 MDN|Function | Functions 指南 | Function.prototype.apply() | 箭頭函式 | this
函式是可以被呼叫的物件,所以它可以有屬性:
function fn() {}
fn.myProp = 1;
console.log(fn.myProp); // 1
console.log(typeof fn); // "function"
console.log(fn instanceof Object); // true
📖 MDN|Map | Set | Keyed collections(Map 與 Object 的取捨)
const m = new Map();
m.set("key", "value"); // 建立 key-value 用 set()
m.get("key"); // 讀取用 get()
m.has("key"); // true
m.delete("key");
m.size; // 用屬性,不是方法(物件用 Object.keys(o).length)
Map 相對於普通物件的四個優勢:
size 直接拿得到for...of
對應題目:LeetCode 2622. Cache With Time Limit、LeetCode 2623. Memoize — 兩題都是拿 Map 當快取,Memoize 那題還會逼你思考「參數怎麼變成一個唯一的 key」。
📖 MDN|WeakMap | WeakMap.prototype.set() | WeakSet | WeakRef | TypeError: key must be an object or an unregistered symbol
📖 支援度|Can I use: WeakMap non-registered symbols as keys
原稿的程式碼有一個變數名打錯,先修正:
const wm = new WeakMap(); // 原稿宣告成 vm 卻用 wm 呼叫
const objKey = { name: "test" };
const symKey = Symbol("weakKey");
wm.set(objKey, "value1");
wm.set(symKey, "value2");
console.log(wm.get(symKey)); // "value2" ✅
try {
wm.set(123, "value3");
} catch (e) {
console.error("報錯:", e.constructor.name, e.message);
// TypeError Invalid value used as weak map key
}
⚠️ 原稿寫「因為數字不是 garbage-collector」— 正確的說法是:數字不是可以被垃圾回收的對象(not garbage-collectable)。WeakMap 的設計前提是「key 沒人引用時,這一筆自動消失」,而原始值沒有身分(identity)也不會被回收,所以不能當 key。
⚠️ 另一個原稿沒提到的細節:只有非註冊的 Symbol 才行。Symbol.for() 建立的是全域註冊表裡的 Symbol,永遠不會消失,所以一樣被擋:
wm.set(Symbol("x"), "v"); // ✅ 可以(ES2023 起)
wm.set(Symbol.for("x"), "v"); // ❌ TypeError: Invalid value used as weak map key
「Symbol 可以當 WeakMap key」是 ES2023(ES14) 才進來的,Chrome 108+ / Node 20+ / Firefox 128+ 才支援,寫給舊環境要注意。

上半段有 console.log 所以看得到 77,下半段只有宣告沒有列印,所以只剩下 ← undefined。

var string2 = "con" 加上 var number2 = 78 得到 "con78",這裡的 77 與 con78 都是字串不是數字。
📌 貼到 ithelp 前的待辦:上面這兩張圖現在指向本機的
obsidian-attachment/,貼上去之前要先把圖片上傳到 iThome,再把括號裡的相對路徑換成 ithelp 回傳的網址(格式是https://ithelp.ithome.com.tw/upload/images/...)。第 1 節那張 typeof 對照表已經是 ithelp 網址,不用動。
仔細看第一張截圖的兩段:
// 第一段(看起來成功)
var cost1 = 7;
var cost2 = '7';
var cost3 = 7 + '7'
console.log(cost3) // ← 有這一行,所以印出 77
// 輸出:77
// 回傳:undefined
// 第二段(看起來失敗)
let string = "concat";
let number1 = 88;
let concatenation_test = string + number1;
// ← 沒有 console.log
// 回傳:undefined
DevTools Console 每次執行都會顯示「這段程式的完成值(completion value)」。而變數宣告語句的完成值永遠是 undefined,不管你用 var 還是 let 還是 const。
第一段之所以看得到 77,是因為 console.log() 主動把值印出來了,那個 undefined 其實也還是有出現(第一張截圖裡 ← undefined 就在下面)。第二段只有宣告沒有列印,所以只剩下 undefined。
所以不是 let 不能做字串相加,是你沒有叫它印出來。
var s1 = "con"; var n1 = 78; console.log(s1 + n1); // con78 string
let s2 = "con"; let n2 = 78; console.log(s2 + n2); // con78 string
const s3 = "con", n3 = 78; console.log(s3 + n3); // con78 string
let s4 = "con"; console.log(s4.concat(78)); // con78
三種宣告方式結果完全一樣。字串相加是 + 運算子的行為,跟你用什麼關鍵字宣告變數毫無關係。
📖 MDN|var | let | const | Hoisting 提升 | TDZ 暫時性死區
var |
let |
const |
|
|---|---|---|---|
| 作用域 | 函式作用域(function scope) | 區塊作用域(block scope) | 區塊作用域 |
| 可否重複宣告 | ✅ 可以 | ❌ SyntaxError | ❌ SyntaxError |
| 可否重新賦值 | ✅ | ✅ | ❌ TypeError |
| 提升(hoisting) | 提升且初始化為 undefined |
提升但進入 TDZ | 提升但進入 TDZ |
掛到 window |
✅ 會(全域宣告時) | ❌ 不會 | ❌ 不會 |
var a = 1; var a = 2; // ✅ 2
let b = 1; let b = 2; // ❌ SyntaxError: Identifier 'b' has already been declared
const c = 1; c = 2; // ❌ TypeError: Assignment to constant variable.
let / const 從區塊開始到宣告那一行之間,變數已經存在但不能存取,碰到就丟 ReferenceError。這是為了逼你「先宣告再使用」。⚠️ 另一個你之後很可能會踩到的坑:在 DevTools Console 裡,同一個頁面連續兩次執行 let x = 1,舊版 Chrome 會噴 SyntaxError: Identifier 'x' has already been declared,而 var 不會。Chrome 80(2020)之後已經特別放寬了 console 的 let 重複宣告,但如果你把同樣的程式貼進 Sources 的 Snippet 或真的 .js 檔案裡,這個限制仍然存在。
+ 運算子的完整規則📖 MDN|Addition (+) | 一元加號 Unary plus (+) | Symbol.toPrimitive | Object.prototype.valueOf() | 型別轉換 Type coercion
📖 規範|ECMA-262: The Addition Operator | ToPrimitive
+ 是 JavaScript 唯一一個「既是加法也是字串連接」的運算子。規則是:
Symbol.toPrimitive → valueOf → toString)1 + 2 // 3 兩邊都是數字
"1" + 2 // "12" 有一邊是字串 → 連接
1 + "2" // "12" 順序不影響
7 + '7' // "77" ← 你截圖裡的例子,結果是字串不是數字
[] + {} // "[object Object]" 兩邊 ToPrimitive 後都變字串
null + "x" // "nullx"
undefined + "x" // "undefinedx"
1 + null // 1 null 轉數字是 0
1 + undefined // NaN undefined 轉數字是 NaN
⚠️ 特別注意你截圖裡的 console.log(cost3) 印出 77:看起來像數字,其實是字串。因為 console.log 印字串時不會加引號。要驗證請用 typeof cost3(結果是 "string")或 console.log({cost3})。
而其他所有算術運算子(- * / %)沒有這個特例,一律轉數字:
"con" - 78 // NaN
"78" - 8 // 70
"3" * "4" // 12
對應題目:LeetCode 2695. Array Wrapper — 這題就是叫你實作
valueOf和toString,親手決定「+遇到我這個物件時要變成什麼」,寫完會徹底理解 ToPrimitive。
Number 的安全上限是 9007199254740991,超過就靜靜掉精度且不報錯;後端 int64 ID 請用字串傳typeof null === "object" 是 bug,判 null 用 === null
new Array(3) 是稀疏陣列,map / forEach 會整個跳過[3] 和 new Array(3) 完全不同Array.isArray() 比 instanceof Array 可靠(跨 realm)+ 有字串特例,- * / 沒有console.log 印字串不加引號,77 可能是 "77"
undefined,那不代表失敗Object.keys / JSON.stringify / for...in 都看不到|| 給預設值會吃掉 0 和 "",要用 ??
== 不是用 truthy / falsy 判斷的,它轉的是數字/原始值。[] 是 truthy 但 [] == false 是 true;null 是 falsy 但 null == false 是 false
null 與 undefined 在 == 的世界裡只跟彼此相等,跟 0、""、false 都不相等iThome鐵人賽-2026/文章-從一個SyntaxError讀懂物件字面量與自動裝箱.mdiThome鐵人賽-2026/文章-閉包活多久-從debounce的timer看Environment-Record存活時間.mdlet timer = null 和第 7 節的 Environment Record,在那篇合成一個完整的生命週期故事。iThome鐵人賽-2026/Map-Symbol-species-規範裡沒人用的屬性.md00-V8引擎完整管線-Parse到Deoptimization.mdJS-資料型別/追問-BigInt取捨-不可變性-與包裝物件.mdGolang make-array.mdmake([]int, 5) 會給你 5 個零值,JS 的 new Array(5) 給你 5 個洞 — 兩個語言在「預先配置長度」上的語意差異,是很容易寫錯的地方。+ 運算子與 ToPrimitivetimer = null
所有 URL 於 2026-09-05 查閱。
本文所有 console 輸出的驗證環境:Node.js v22.22.2,執行日期 2026-09-05。